package drds.plus.parser.visitor;

import drds.plus.parser.abstract_syntax_tree.Node;
import drds.plus.parser.abstract_syntax_tree.expression.BinaryExpression;
import drds.plus.parser.abstract_syntax_tree.expression.Expression;
import drds.plus.parser.abstract_syntax_tree.expression.Pair;
import drds.plus.parser.abstract_syntax_tree.expression.UnaryExpression;
import drds.plus.parser.abstract_syntax_tree.expression.comparison.Is;
import drds.plus.parser.abstract_syntax_tree.expression.comparison.fuzzy_matching.Like;
import drds.plus.parser.abstract_syntax_tree.expression.comparison.range.BetweenAnd;
import drds.plus.parser.abstract_syntax_tree.expression.comparison.range.Equal;
import drds.plus.parser.abstract_syntax_tree.expression.comparison.range.In;
import drds.plus.parser.abstract_syntax_tree.expression.logical.And;
import drds.plus.parser.abstract_syntax_tree.expression.logical.ListExpression;
import drds.plus.parser.abstract_syntax_tree.expression.logical.Or;
import drds.plus.parser.abstract_syntax_tree.expression.primary.function.Function;
import drds.plus.parser.abstract_syntax_tree.expression.primary.function.aggregation.*;
import drds.plus.parser.abstract_syntax_tree.expression.primary.literal.LiteralBoolean;
import drds.plus.parser.abstract_syntax_tree.expression.primary.literal.LiteralNull;
import drds.plus.parser.abstract_syntax_tree.expression.primary.literal.LiteralNumber;
import drds.plus.parser.abstract_syntax_tree.expression.primary.literal.LiteralString;
import drds.plus.parser.abstract_syntax_tree.expression.primary.misc.Identifier;
import drds.plus.parser.abstract_syntax_tree.expression.primary.misc.InList;
import drds.plus.parser.abstract_syntax_tree.expression.primary.misc.ParameterMarker;
import drds.plus.parser.abstract_syntax_tree.expression.primary.misc.RowValues;
import drds.plus.parser.abstract_syntax_tree.expression.primary.subquery.Exists;
import drds.plus.parser.abstract_syntax_tree.statement.*;
import drds.plus.parser.abstract_syntax_tree.statement.select.GroupBy;
import drds.plus.parser.abstract_syntax_tree.statement.select.Limit;
import drds.plus.parser.abstract_syntax_tree.statement.select.Order;
import drds.plus.parser.abstract_syntax_tree.statement.select.OrderBy;
import drds.plus.parser.abstract_syntax_tree.statement.select.table.*;

import java.util.List;

public class VisitorImpl implements Visitor {

    protected static final Object[] EMPTY_OBJ_ARRAY = new Object[0];
    protected static final int[] EMPTY_INT_ARRAY = new int[0];
    protected final StringBuilder sb;
    protected final Object[] args;
    protected int[] argsIndex;

    protected int index = -1;

    public VisitorImpl(StringBuilder sb) {
        this(sb, null);
    }


    public VisitorImpl(StringBuilder sb, Object[] args) {
        this.sb = sb;
        this.args = args == null ? EMPTY_OBJ_ARRAY : args;
        this.argsIndex = args == null ? EMPTY_INT_ARRAY : new int[args.length];
    }


    public String getSql() {
        return sb.toString();
    }


    ////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * @param list never null
     */
    protected void printList(List<? extends Node> list) {
        printList(list, ", ");
    }

    /**
     * @param list never null
     */
    protected void printList(List<? extends Node> list, String sep) {
        boolean first = true;
        for (Node arg : list) {
            if (first)
                first = false;
            else
                sb.append(sep);
            arg.accept(this);
        }
    }


    ///////////////////////////////////////////////////////////////////////////////////////////////
    public void visit(Identifier identifier) {
        Expression parent = identifier.getParent();
        if (parent != null) {
            parent.accept(this);
            sb.append('.');
        }
        sb.append(identifier.getText());
    }

    public void visit(ParameterMarker parameterMarker) {
        sb.append('?');
        appendArgsIndex(parameterMarker.getParameterIndex() - 1);
    }

    protected void appendArgsIndex(int value) {
        int i = ++index;
        if (argsIndex.length <= i) {
            int[] a = new int[i + 1];
            if (i > 0)
                System.arraycopy(argsIndex, 0, a, 0, i);
            argsIndex = a;
        }
        argsIndex[i] = value;
    }

    public void visit(RowValues rowValues) {
        sb.append("row(");
        printList(rowValues.getRowValueList());
        sb.append(')');
    }
    //

    public void visit(InsertStatement insertStatement) {
        sb.append("insert ");
        sb.append("into ");
        insertStatement.getTableName().accept(this);
        sb.append(' ');

        List<Identifier> cols = insertStatement.getColumnNameList();
        if (cols != null && !cols.isEmpty()) {
            sb.append('(');
            printList(cols);
            sb.append(") ");
        }

        Query select = insertStatement.getQuery();
        if (select == null) {
            sb.append("values ");
            List<RowValues> rowValuesList = insertStatement.getRowValuesList();
            if (rowValuesList != null && !rowValuesList.isEmpty()) {
                boolean first = true;
                for (RowValues rowValues : rowValuesList) {
                    if (rowValues == null || rowValues.getRowValueList().isEmpty())
                        continue;
                    if (first)
                        first = false;
                    else
                        sb.append(", ");
                    sb.append('(');
                    printList(rowValues.getRowValueList());
                    sb.append(')');
                }
            } else {
                throw new IllegalArgumentException("at least one row for insert");
            }
        } else {
            select.accept(this);
        }

        List<Pair<Identifier, Expression>> dup = insertStatement.getDuplicateUpdate();
        if (dup != null && !dup.isEmpty()) {
            sb.append(" on duplicate key update ");
            boolean first = true;
            for (Pair<Identifier, Expression> pair : dup) {
                if (first)
                    first = false;
                else
                    sb.append(", ");
                pair.getKey().accept(this);
                sb.append(" = ");
                pair.getValue().accept(this);
            }
        }
    }

    public void visit(ReplaceStatement replaceStatement) {
        sb.append("replace ");
        sb.append("into ");
        replaceStatement.getTableName().accept(this);
        sb.append(' ');

        List<Identifier> columnNameList = replaceStatement.getColumnNameList();
        if (columnNameList != null && !columnNameList.isEmpty()) {
            sb.append('(');
            printList(columnNameList);
            sb.append(") ");
        }

        Query select = replaceStatement.getQuery();
        if (select == null) {
            sb.append("values ");
            List<RowValues> rowValuesList = replaceStatement.getRowValuesList();
            if (rowValuesList != null && !rowValuesList.isEmpty()) {
                boolean first = true;
                for (RowValues rowValues : rowValuesList) {
                    if (rowValues == null || rowValues.getRowValueList().isEmpty())
                        continue;
                    if (first)
                        first = false;
                    else
                        sb.append(", ");
                    sb.append('(');
                    printList(rowValues.getRowValueList());
                    sb.append(')');
                }
            } else {
                throw new IllegalArgumentException("at least one row for REPLACE");
            }
        } else {
            select.accept(this);
        }
    }

    public void visit(UpdateStatement updateStatement) {
        sb.append("update ");
        updateStatement.getTable().accept(this);
        sb.append(" set ");
        boolean first = true;
        for (Pair<Identifier, Expression> pair : updateStatement.getValuePairList()) {
            if (first)
                first = false;
            else
                sb.append(", ");
            pair.getKey().accept(this);
            sb.append(" = ");

            Expression value = pair.getValue();

            boolean paren = value.getPriority() <= Expression.precedence_comparision;
            if (paren)
                sb.append('(');
            value.accept(this);
            if (paren)
                sb.append(')');
        }
        Expression where = updateStatement.getWhere();
        if (where != null) {
            sb.append(" where ");
            where.accept(this);
        }
    }

    public void visit(DeleteStatement deleteStatement) {
        sb.append("delete ");
        sb.append("from ");
        deleteStatement.getTable().accept(this);
        Expression where = deleteStatement.getWhere();
        if (where != null) {
            sb.append(" where ");
            where.accept(this);
        }
    }

    public void visit(TruncateStatement truncateStatement) {
        sb.append("truncate table ");
        truncateStatement.getTableName().accept(this);
    }

    public void visit(SelectStatement selectStatement) {
        sb.append("query ");
        boolean first = true;
        List<Pair<Expression, String>> pairList = selectStatement.getSelectItemPairList();
        for (Pair<Expression, String> pair : pairList) {
            if (first)
                first = false;
            else
                sb.append(", ");
            pair.getKey().accept(this);
            String alias = pair.getValue();
            if (alias != null) {
                sb.append(" as ").append(alias);
            }
        }

        Tables from = selectStatement.getTables();
        if (from != null) {
            sb.append(" from ");
            from.accept(this);
        }

        Expression where = selectStatement.getWhere();
        if (where != null) {
            sb.append(" where ");
            where.accept(this);
        }

        GroupBy groupBy = selectStatement.getGroupBy();
        if (groupBy != null) {
            sb.append(' ');
            groupBy.accept(this);
        }

        Expression having = selectStatement.getHaving();
        if (having != null) {
            sb.append(" having ");
            having.accept(this);
        }

        OrderBy order = selectStatement.getOrderBy();
        if (order != null) {
            sb.append(' ');
            order.accept(this);
        }

        Limit limit = selectStatement.getLimit();
        if (limit != null) {
            sb.append(' ');
            limit.accept(this);
        }
        if (selectStatement.isLockInShareMode()) {
            sb.append(" lock in share mode");
        }
        if (selectStatement.isForUpdate()) {
            sb.append(" for update");

        }
    }

    public void visit(UnionStatement unionStatement) {
        List<SelectStatement> selectStatementList = unionStatement.getSelectStatementList();
        if (selectStatementList == null || selectStatementList.isEmpty()) {
            throw new IllegalArgumentException("query union must have at least one query");
        }
        final int fstDist = unionStatement.getFirstDistinctIndex();
        int i = 0;
        for (SelectStatement selectStatement : selectStatementList) {
            if (i > 0) {
                sb.append(" union ");
                if (i > fstDist) {
                    sb.append("all ");
                }
            }
            sb.append('(');
            selectStatement.accept(this);
            sb.append(')');
            ++i;
        }
        OrderBy orderBy = unionStatement.getOrderBy();
        if (orderBy != null) {
            sb.append(' ');
            orderBy.accept(this);
        }
        Limit limit = unionStatement.getLimit();
        if (limit != null) {
            sb.append(' ');
            limit.accept(this);
        }
    }

    //
    public void visit(OrderBy orderBy) {
        sb.append("order by ");
        boolean first = true;
        for (Pair<Expression, Order> pair : orderBy.getOrderByList()) {
            if (first)
                first = false;
            else
                sb.append(", ");
            Expression column = pair.getKey();
            column.accept(this);
            switch (pair.getValue()) {
                case asc:
                    sb.append(" asc");
                    break;
                case desc:
                    sb.append(" desc");
                    break;
            }
        }
    }

    public void visit(Limit limit) {
        sb.append("limit ");
        Object offset = limit.getOffset();
        if (offset instanceof ParameterMarker) {
            ((ParameterMarker) offset).accept(this);
        } else {
            sb.append(offset);
        }
        sb.append(", ");
        Object size = limit.getSize();
        if (size instanceof ParameterMarker) {
            ((ParameterMarker) size).accept(this);
        } else {
            sb.append(size);
        }
    }

    public void visit(GroupBy groupBy) {
        sb.append("group by ");
        boolean first = true;
        for (Pair<Expression, Order> pair : groupBy.getGroupByList()) {
            if (first)
                first = false;
            else
                sb.append(", ");
            Expression column = pair.getKey();
            column.accept(this);
            switch (pair.getValue()) {
                case desc:
                    sb.append(" desc");
                    break;
            }
        }

    }
    //


    public void visit(InnerJoin innerJoin) {
        ITable left = innerJoin.getLeftTable();
        boolean paren = left.getPriority() < innerJoin.getPriority();
        if (paren)
            sb.append('(');
        left.accept(this);
        if (paren)
            sb.append(')');

        sb.append(" inner join ");
        ITable right = innerJoin.getRightTable();
        paren = right.getPriority() <= innerJoin.getPriority();
        if (paren)
            sb.append('(');
        right.accept(this);
        if (paren)
            sb.append(')');

        Expression on = innerJoin.getOn();
        if (on != null) {
            sb.append(" on ");
            on.accept(this);
        }
    }


    public void visit(OuterJoin outerJoin) {
        ITable left = outerJoin.getLeftTable();
        boolean paren = left.getPriority() < outerJoin.getPriority();
        if (paren)
            sb.append('(');
        left.accept(this);
        if (paren)
            sb.append(')');

        if (outerJoin.isLeftJoin())
            sb.append(" left join ");
        else
            sb.append(" right join ");

        ITable right = outerJoin.getRightTable();
        paren = right.getPriority() <= outerJoin.getPriority();
        if (paren)
            sb.append('(');
        right.accept(this);
        if (paren)
            sb.append(')');

        Expression on = outerJoin.getOn();

        if (on != null) {
            sb.append(" on ");
            on.accept(this);
        } else {
            throw new IllegalArgumentException("either on or using must be included for outer join");
        }
    }

    public void visit(SubQuery subQuery) {
        sb.append('(');
        Query query = subQuery.getQuery();
        query.accept(this);
        sb.append(") as ").append(subQuery.getAlias());
    }

    public void visit(Exists exists) {
        sb.append("exists (");
        exists.getQuery().accept(this);
        sb.append(')');
    }

    public void visit(Table tableRef) {
        Identifier table = tableRef.getTableName();
        table.accept(this);
        String alias = tableRef.getAlias();
        if (alias != null) {
            sb.append(" as ").append(alias);
        }
    }

    public void visit(Tables tables) {
        printList(tables.getTableList());
    }


    //
    public void visit(Function function) {
        String functionName = function.getFunctionName();
        sb.append(functionName).append('(');
        printList(function.getArgList());
        sb.append(')');
    }

    public void visit(Max max) {
        String functionName = max.getFunctionName();
        sb.append(functionName).append('(');
        if (max.isDistinct()) {
            sb.append("distinct ");
        }
        printList(max.getArgList());
        sb.append(')');
    }

    public void visit(Min min) {
        String functionName = min.getFunctionName();
        sb.append(functionName).append('(');
        if (min.isDistinct()) {
            sb.append("distinct ");
        }
        printList(min.getArgList());
        sb.append(')');
    }

    public void visit(Sum sum) {
        String functionName = sum.getFunctionName();
        sb.append(functionName).append('(');
        if (sum.isDistinct()) {
            sb.append("distinct ");
        }
        printList(sum.getArgList());
        sb.append(')');
    }

    public void visit(Count count) {
        String functionName = count.getFunctionName();
        sb.append(functionName).append('(');
        if (count.isDistinct()) {
            sb.append("distinct ");
        }
        printList(count.getArgList());
        sb.append(')');
    }

    public void visit(Avg avg) {
        String functionName = avg.getFunctionName();
        sb.append(functionName).append('(');
        if (avg.isDistinct()) {
            sb.append("distinct ");
        }
        printList(avg.getArgList());
        sb.append(')');
    }
    //

    public void visit(LiteralNull literalNull) {
        sb.append("null");
    }

    public void visit(LiteralBoolean literalBoolean) {
        if (literalBoolean.isTrue()) {
            sb.append("true");
        } else {
            sb.append("false");
        }
    }

    public void visit(LiteralNumber literalNumber) {
        sb.append(literalNumber.getNumber());
    }

    public void visit(LiteralString literalString) {
        sb.append('\'').append(literalString.getString()).append('\'');
    }

    //

    public void visit(UnaryExpression unaryExpression) {
        sb.append(unaryExpression.getOperator()).append(' ');
        boolean paren = unaryExpression.getExpression().getPriority() < unaryExpression.getPriority();
        if (paren)
            sb.append('(');
        unaryExpression.getExpression().accept(this);
        if (paren)
            sb.append(')');
    }

    public void visit(BinaryExpression binaryExpression) {
        Expression left = binaryExpression.getLeft();
        boolean paren = binaryExpression.isLeftCombine() ? left.getPriority() < binaryExpression.getPriority() : left.getPriority() <= binaryExpression.getPriority();
        if (paren)
            sb.append('(');
        left.accept(this);
        if (paren)
            sb.append(')');

        sb.append(' ').append(binaryExpression.getOperator()).append(' ');

        Expression right = binaryExpression.getRight();
        paren = binaryExpression.isLeftCombine() ? right.getPriority() <= binaryExpression.getPriority() : right.getPriority() < binaryExpression.getPriority();
        if (paren)
            sb.append('(');
        right.accept(this);
        if (paren)
            sb.append(')');
    }

    //
    public void visit(Or or) {
        visit((ListExpression) or);
    }

    public void visit(And and) {
        visit((ListExpression) and);
    }

    public void visit(Equal equal) {
        visit((BinaryExpression) equal);
    }


    public void visit(Is is) {
        Expression comparee = is.getExpression();
        boolean paren = comparee.getPriority() < is.getPriority();
        if (paren)
            sb.append('(');
        comparee.accept(this);
        if (paren)
            sb.append(')');
        switch (is.getMode()) {
            case Is.IS_NULL:
                sb.append(" is null");
                break;
            case Is.IS_TRUE:
                sb.append(" is true");
                break;
            case Is.IS_FALSE:
                sb.append(" is false");
                break;

            case Is.IS_NOT_NULL:
                sb.append(" is not null");
                break;
            case Is.IS_NOT_TRUE:
                sb.append(" is not true");
                break;
            case Is.IS_NOT_FALSE:
                sb.append(" is not false");
                break;

            default:
                throw new IllegalArgumentException("unknown mode for IS expression: " + is.getMode());
        }
    }

    public void visit(Like like) {
        Expression comparee = like.getComparee();
        boolean paren = comparee.getPriority() < like.getPriority();
        if (paren)
            sb.append('(');
        comparee.accept(this);
        if (paren)
            sb.append(')');

        if (like.isNot())
            sb.append(" not like ");
        else
            sb.append(" like ");

        Expression pattern = like.getPattern();
        paren = pattern.getPriority() <= like.getPriority();
        if (paren)
            sb.append('(');
        pattern.accept(this);
        if (paren)
            sb.append(')');

        Expression escape = like.getEscape();
        if (escape != null) {
            sb.append(" escape ");
            paren = escape.getPriority() <= like.getPriority();
            if (paren)
                sb.append('(');
            escape.accept(this);
            if (paren)
                sb.append(')');
        }
    }

    public void visit(BetweenAnd betweenAnd) {
        Expression comparee = betweenAnd.getComparee();
        boolean paren = comparee.getPriority() <= betweenAnd.getPriority();
        if (paren)
            sb.append('(');
        comparee.accept(this);
        if (paren)
            sb.append(')');

        if (betweenAnd.isNot())
            sb.append(" not between ");
        else
            sb.append(" between ");

        Expression start = betweenAnd.getNotLessThan();
        paren = start.getPriority() < betweenAnd.getPriority();
        if (paren)
            sb.append('(');
        start.accept(this);
        if (paren)
            sb.append(')');

        sb.append(" and ");

        Expression end = betweenAnd.getNotGreaterThan();
        paren = end.getPriority() < betweenAnd.getPriority();
        if (paren)
            sb.append('(');
        end.accept(this);
        if (paren)
            sb.append(')');
    }


    public void visit(In in) {
        visit((BinaryExpression) in);
    }


    public void visit(ListExpression listExpression) {
        for (int i = 0, len = listExpression.size(); i < len; ++i) {
            if (i > 0)
                sb.append(' ').append(listExpression.getOperator()).append(' ');
            Expression operand = listExpression.getExpression(i);
            boolean paren = operand.getPriority() < listExpression.getPriority();
            if (paren)
                sb.append('(');
            operand.accept(this);
            if (paren)
                sb.append(')');
        }
    }

    public void visit(InList inList) {
        sb.append('(');
        printList(inList.getList());
        sb.append(')');
    }

    //


}
